/**
 * @brief Space Invaders game implementation
 * @author Gilad Avidov
 *
 */
#include <stdio.h>
#include "common.h"
#include "lcd.h"
#include "space.h"
#include "joystick.h"
#include "sched.h"

#define SPCAE_EXPLOSION_MAX_CNT (5)
#define SPCAE_JSTCK_SPEED_X(x)  (x / 64)
#define SPCAE_JSTCK_SPEED_Y(y)  (y / 64)
#define SPEED_NONE              (125)
#define SPCAE_SPEED_X(x)        (x)
#define SPCAE_SPEED_Y(y)        (y)
#define SPACE_X_TO_LCD(x)      ((x) >> 4)
#define SPACE_Y_TO_LCD(y)      ((y) >> 6)
#define LCD_X_TO_SPACE(x)      ((x) << 4)
#define LCD_Y_TO_SPACE(y)      ((y) << 6)
#define SPACE_MAX_X             (LCD_X_TO_SPACE(LCD_MAX_X + 1) - 2)
#define SPACE_MAX_Y             (LCD_Y_TO_SPACE(LCD_MAX_Y + 1) - 2)
#define AVATAR_MIN_Y            ((LCD_Y_TO_SPACE(2) + LCD_Y_TO_SPACE(3)) / 2)
#define INVADERS_MAX_CNT        (LCD_MAX_X - 2)
#define SPCAE_LEVEL_DELAY       usleep(0x8FFF)
#define SPCAE_LEVEL_MAX         (INVADERS_MAX_CNT - 1)
#define SPCAE_LEVEL_CHANGE_TIMEOUT (20) // let time for explosions finish gracefully before new level
#define SPACE_NEW_EXPLOSION        (8)
#define sprite_goto_xy(explosion) \
    lcdgotoxy(SPACE_Y_TO_LCD(explosion->y), SPACE_X_TO_LCD(explosion->x))

typedef enum {
    SPACE_AVATAR,
    SPACE_INVADER_SHAPE_1,
    SPACE_INVADER_SHAPE_2,
    SPACE_INVADER_SHAPE_3,
    SPACE_INVADER_SHAPE_4,
    SPACE_EXPLOTION_SMALL,
    SPACE_EXPLOTION_BIG,
} space_type_t ;

typedef unsigned char lcd_custom_char_t[LCD_CHAR_ROWS];

typedef struct {
    unsigned char shape;
    unsigned char lcd_x;
    unsigned char lcd_y;
} space_explosion_t;

typedef struct {
    unsigned char x;
    unsigned char y;
    unsigned char is_dead;
} space_avatar_sprite_t;

typedef struct {
    unsigned char shape_num;
    short x;
    unsigned char y;
    char x_speed;
    unsigned short alive_bitset; /* bit index is (LCD_MAX_X - x) position on the lcd */
} space_sprite_invader_t;

typedef struct {
    unsigned char shape;
    unsigned char x;
    unsigned char y;
    char y_speed;
} space_sprite_missile_t;

typedef struct {
    unsigned char done:1;
    unsigned char level:7;
    unsigned char level_change_delay;
    space_explosion_t explosions[SPCAE_EXPLOSION_MAX_CNT];
    space_avatar_sprite_t    sprite_avatar;
    space_sprite_invader_t   sprite_invaders;
    space_sprite_missile_t   sprite_missile;
} space_t;

static space_t space;
static const char lcd_empty_line[LCD_MAX_X + 2] ="                ";
static const char lcd_line[LCD_MAX_X + 2]       ="+--------------+";
static const char lcd_title[LCD_MAX_X + 2]      ="|Space Invaders|";
static char space_explosion_shape[8];


void lcd_clear_screen(void)
{
    lcdgotoxy(0,0);  lcdputstr(lcd_empty_line);
    lcdgotoxy(1,0);  lcdputstr(lcd_empty_line);
    lcdgotoxy(2,0);  lcdputstr(lcd_empty_line);
    lcdgotoxy(3,0);  lcdputstr(lcd_empty_line);
}

void lcd_erase(unsigned char x, unsigned char y)
{
    lcdgotoxy(y,x);
    lcdputch(' ');
}

void lcd_show(unsigned char x, unsigned char y, unsigned char ch)
{
	lcdgotoxy(y,x);
    lcdputch(ch);
}

void space_create_lcd_custom_chars()
{
    const lcd_custom_char_t lcd_space_images[LCD_CHAR_CUSTOM_CNT] = {
        {0x4, 0x4, 0x4, 0xe, 0x1f, 0x15, 0x4, 0xe},     // SPACE_AVATAR
        {0x4, 0xe, 0x15, 0x1f, 0xa, 0x4, 0xa, 0x15},    // SPACE_INVADER_SHAPE_1
        {0x4, 0xe, 0x15, 0x1f, 0xa, 0xa, 0xe, 0x4},     // SPACE_INVADER_SHAPE_2
        {0x11, 0x11, 0x11, 0xe, 0x15, 0x1f, 0x1b, 0xe}, // SPACE_INVADER_SHAPE_3
        {0xa, 0xe, 0x15, 0x1b, 0xe, 0xa, 0xe, 0x11},    // SPACE_INVADER_SHAPE_4
        {0x8, 0x1, 0x4, 0xa, 0x10, 0x2, 0x8, 0x2},      // SPACE_EXPLOTION_SMALL
        {0x4, 0x15, 0xe, 0x1f, 0xe, 0x15, 0xe, 0x14},   // SPACE_EXPLOTION_BIG
    };

    lcdcreate_all_chars(lcd_space_images);
}

void space_start_page(void)
{
    lcdgotoxy(0,0); lcdputstr(lcd_line);
    lcdgotoxy(1,0); lcdputstr(lcd_title);
    lcdgotoxy(2,0); lcdputstr("| Gilad Avidov |");
    lcdgotoxy(3,0); lcdputstr(lcd_line);
    SPCAE_LEVEL_DELAY;
    SPCAE_LEVEL_DELAY;
}

void space_winner_page(void)
{
    lcdgotoxy(0,0); lcdputstr(lcd_line);
    lcdgotoxy(1,0); lcdputstr("|You have saved|");
    lcdgotoxy(2,0); lcdputstr("|  the Earth   |");
    lcdgotoxy(3,0); lcdputstr(lcd_line);
    SPCAE_LEVEL_DELAY;
}

/* return non zero if ongoing */
unsigned char space_explosion_step(void)
{
    space_explosion_t *explosion = space.explosions;
    unsigned char i;
    unsigned char is_ongoing = 0;

    for (i = 0 ; i < SPCAE_EXPLOSION_MAX_CNT ; ++i, ++explosion)
    {
        if (!explosion->shape)
            continue;

        lcd_show(explosion->lcd_x, explosion->lcd_y, space_explosion_shape[explosion->shape - 1]);
        --explosion->shape;
        is_ongoing = 1;
    }
    return is_ongoing;
}

#define SPACE_EXPLOSION_MIN     (100)
#define SPACE_EXPLOSION_TIME    (100)

/* return zero on success */
unsigned char space_explosion_spwn(unsigned char lcd_x, unsigned char lcd_y)
{
    space_explosion_t *explosion = space.explosions;
    unsigned char i;

    //printf("adding explosion at %d %d\n\r", lcd_x,lcd_y);

    for (i = 0 ; i < SPCAE_EXPLOSION_MAX_CNT ; ++i, ++explosion)
        if (!explosion->shape) {
            //printf("adding explosion at %d %d i:%d\n\r", lcd_x,lcd_y, i);
            break;
        }

    if (!(i < SPCAE_EXPLOSION_MAX_CNT)){
        //printf("adding explosion no space for more\n\r", lcd_x,lcd_y, i);
        return 1;
    }

    explosion->lcd_x   = lcd_x;
    explosion->lcd_y   = lcd_y;
    explosion->shape   = SPACE_NEW_EXPLOSION;

    return 0;
}

#define SPACE_MISSILE_END       (4)

/* there is at most one missile which is shot by the avatar. it is going straight up */
void space_missile_step(void)
{
    static bit shape_change = 0;
    bit hit_end = 0;
    static char space_missile_shape[4] = {'\'', '|',',', '.'};

    short y = space.sprite_missile.y_speed;
    unsigned char lcd_x, lcd_y = 0;
    short val;
    unsigned char prev_lcd_y = SPACE_Y_TO_LCD(space.sprite_missile.y);
    unsigned char y_fine;

    if (space.sprite_missile.shape == SPACE_MISSILE_END)
        return;

    lcd_x = SPACE_X_TO_LCD(space.sprite_missile.x);

    val = SPCAE_SPEED_Y(y) + space.sprite_missile.y;
    if (val < 0) {
        val = 0;
        space.sprite_missile.shape = SPACE_MISSILE_END;
        lcd_erase(lcd_x, prev_lcd_y);
        return;
    }
    space.sprite_missile.y = val;
    y_fine = ((space.sprite_missile.y >> 4) & 3);
    lcd_y = SPACE_Y_TO_LCD(space.sprite_missile.y);

    // hit an invader
    if (lcd_y == SPACE_Y_TO_LCD(space.sprite_invaders.y)) {
        unsigned short mask = BIT(LCD_MAX_X - lcd_x);
        //printf("missile y=invaedaers y:%d  x:%d sprites:0x%04x mask:0x%x\n\r",
        //       lcd_y, lcd_x, space.sprite_invaders.alive_bitset, mask);
        if (space.sprite_invaders.alive_bitset & mask) {
            space.sprite_invaders.alive_bitset &= ~mask;
            space.sprite_missile.shape = SPACE_MISSILE_END;
            lcd_erase(lcd_x, prev_lcd_y);
            space_explosion_spwn(lcd_x, lcd_y);
            return;
        }
    }

    if (lcd_y != prev_lcd_y)
        lcd_erase(lcd_x, prev_lcd_y);

    lcdgotoxy(lcd_y,lcd_x);
    lcdputch(space_missile_shape[y_fine]);
}

void space_init_level(void)
{
    unsigned char i;
    char level_msg[LCD_MAX_X];
    space_explosion_t *explosion = space.explosions;

    if (space.level > SPCAE_LEVEL_MAX) {
        space_winner_page();
        space.level = 1;
    }

    for (i = 0 ; i < SPCAE_EXPLOSION_MAX_CNT ; ++i, ++explosion)
        explosion->shape = 0;

    space.sprite_avatar.x         = LCD_X_TO_SPACE(LCD_MAX_X / 2);
    space.sprite_avatar.y         = LCD_Y_TO_SPACE(3);
    space.sprite_avatar.is_dead   = 0;

    space.sprite_invaders.shape_num = 0;
    space.sprite_invaders.x         = 0;
    space.sprite_invaders.y         = 0;
    space.sprite_invaders.x_speed   = space.level * 2;

    space.sprite_missile.shape = SPACE_MISSILE_END;
    space.sprite_invaders.alive_bitset = (0xFFFF >> (INVADERS_MAX_CNT - space.level));

    space.done = 0;

    sprintf(level_msg,"|   Level %02u   |", space.level);
    lcdgotoxy(0,0); lcdputstr(lcd_line);
    lcdgotoxy(1,0); lcdputstr(lcd_title);
    lcdgotoxy(2,0); lcdputstr(level_msg);
    lcdgotoxy(3,0); lcdputstr(lcd_line);
    SPCAE_LEVEL_DELAY;
    lcd_clear_screen();
}

void space_avatar_blowup(void)
{
    unsigned char lcd_x = SPACE_X_TO_LCD(space.sprite_avatar.x);
    unsigned char lcd_y = SPACE_Y_TO_LCD(space.sprite_avatar.y);

    space.sprite_avatar.is_dead = 1;

    // give some space for nice explosion
    if (lcd_x == 0)
        lcd_x = 1;
    if (lcd_x >= LCD_MAX_X)
        lcd_x = LCD_MAX_X-1;

    space_explosion_spwn(lcd_x,lcd_y);
    if (lcd_y == LCD_MAX_Y){
        space_explosion_spwn(lcd_x - 1,lcd_y - 1);
        space_explosion_spwn(lcd_x + 1,lcd_y - 1);
    } else {
        space_explosion_spwn(lcd_x - 1,lcd_y - 1);
        space_explosion_spwn(lcd_x + 1,lcd_y - 1);
        space_explosion_spwn(lcd_x - 1,lcd_y + 1);
        space_explosion_spwn(lcd_x + 1,lcd_y + 1);
    }

    if (space.level > 1)
        --space.level;
    space.level_change_delay = SPCAE_LEVEL_CHANGE_TIMEOUT;
}

#define INVADAERS_SHAPE_CHANGE_TIMEOUT    (5)
#define INVADAERS_SHAPE_CNT               (4)
void space_invaders_step(void)
{
    static unsigned char shape_change_timeout = INVADAERS_SHAPE_CHANGE_TIMEOUT;
    bit hit_end = 0;
    char shape;
    unsigned short mask;
    short x = space.sprite_invaders.x_speed;
    char lcd_x, lcd_y = 0;
    char prev_lcd_x = SPACE_X_TO_LCD(space.sprite_invaders.x);
    char prev_lcd_y = SPACE_Y_TO_LCD(space.sprite_invaders.y);

    if (!space.sprite_invaders.alive_bitset) {
        if (!space.level_change_delay)
            space.level_change_delay = SPCAE_LEVEL_CHANGE_TIMEOUT;
        return;
    }

    space.sprite_invaders.x += SPCAE_SPEED_X(x);
    lcd_x = SPACE_X_TO_LCD(space.sprite_invaders.x);

    if (lcd_x < prev_lcd_x) {
        hit_end = (space.sprite_invaders.alive_bitset & BIT(0));
        if (!hit_end)
            space.sprite_invaders.alive_bitset >>= 1;
        //printf("invaders lcd_x:%d prev_lcd_x:%d space.sprite_invaders.alive_bitset >>= 1:0x%x hit_end:%d\n\r",
        //       lcd_x, prev_lcd_x, space.sprite_invaders.alive_bitset, hit_end);
    } else if (lcd_x > prev_lcd_x) {
        hit_end = (space.sprite_invaders.alive_bitset & BIT(LCD_MAX_X));
        if (!hit_end)
            space.sprite_invaders.alive_bitset <<= 1;
        //printf("invaders lcd_x:%u prev_lcd_x:%u space.sprite_invaders.alive_bitset <<= 1:0x%x hit_end:%d\n\r",
        //       lcd_x, prev_lcd_x, space.sprite_invaders.alive_bitset, hit_end);
    }

   if (hit_end) {
         space.sprite_invaders.x_speed = -space.sprite_invaders.x_speed;

       //erase prev line of invaders
        lcdgotoxy(prev_lcd_y,0);
        lcdputstr(lcd_empty_line);

        lcd_y = prev_lcd_y + 1;
        // invasion
        if (lcd_y > 2) {
            lcd_y = 0;
            space_avatar_blowup();
        }

        space.sprite_invaders.y = LCD_Y_TO_SPACE(lcd_y);
    } else {
        lcd_y = SPACE_Y_TO_LCD(space.sprite_invaders.y);
    }

    // put spaces from beginning of line to first invader
    lcdgotoxy(lcd_y,0);
    for (mask = BIT(LCD_MAX_X) ; mask ; mask >>= 1) {
        if (space.sprite_invaders.alive_bitset & mask) {
            shape = SPACE_INVADER_SHAPE_1 + space.sprite_invaders.shape_num;
        } else {
            shape = ' ';
        }
        lcdputch(shape);
    }
    if (!(--shape_change_timeout)){
        if ((++space.sprite_invaders.shape_num) >= INVADAERS_SHAPE_CNT)
            space.sprite_invaders.shape_num = 0;
        shape_change_timeout = INVADAERS_SHAPE_CHANGE_TIMEOUT;
    }
}

void space_avatar_step(void)
{
    short x = joystick_get_x();
    short y = joystick_get_y();
    unsigned char lcd_x, lcd_y;
    unsigned char prev_lcd_x = SPACE_X_TO_LCD(space.sprite_avatar.x);
    unsigned char prev_lcd_y = SPACE_Y_TO_LCD(space.sprite_avatar.y);
    short val;

    if (space.sprite_avatar.is_dead)
        return;

    val = SPCAE_JSTCK_SPEED_X(x) + space.sprite_avatar.x ;
    //printf("avatar jstk_x:%d jystk_spd_x:%d val:%d\n\r",
    //       x, SPCAE_JSTCK_SPEED_X(x), val);
    if (val > SPACE_MAX_X)
        val = SPACE_MAX_X;
    if (val < 0)
        val = 0;
    space.sprite_avatar.x = val;
    lcd_x = SPACE_X_TO_LCD(space.sprite_avatar.x);

    val = SPCAE_JSTCK_SPEED_Y(y) + space.sprite_avatar.y;
    if (val > SPACE_MAX_Y)
        val = SPACE_MAX_Y;
    else if (val < AVATAR_MIN_Y)
        val = AVATAR_MIN_Y;
    space.sprite_avatar.y = val;

    lcd_y = SPACE_Y_TO_LCD(space.sprite_avatar.y);

    // avatar
    if ((lcd_x != prev_lcd_x) || (lcd_y != prev_lcd_y))
        lcd_erase(prev_lcd_x, prev_lcd_y);

    lcd_show(lcd_x, lcd_y, SPACE_AVATAR);
    // launch
    if (joystick_button_pressed() && (space.sprite_missile.shape == SPACE_MISSILE_END)) {
        space.sprite_missile.shape = 0;
        space.sprite_missile.x = space.sprite_avatar.x;
        space.sprite_missile.y = space.sprite_avatar.y;
        space.sprite_missile.y_speed = -10;
    }

    //space_missile_step();
}

#define SPCAE_FREQ_SPRITES      (6)
#define SPCAE_FREQ_MISSLE       (2)
#define SPCAE_FREQ_EXPLOSION    (3)

void space_step(void)
{
    static unsigned char cnt = 0;

    if (!(cnt % SPCAE_FREQ_MISSLE))
        space_missile_step();

    space_avatar_step();
    if (!(cnt % SPCAE_FREQ_SPRITES))
        space_invaders_step();

    if (!(cnt % SPCAE_FREQ_EXPLOSION))
        space_explosion_step();

    if (space.level_change_delay) {
        --space.level_change_delay;
        if (!space.level_change_delay){
            ++space.level;
            space_init_level();
        }
    }

    ++cnt;
}

void space_avatar_thread(void)
{
    while (1){
        space_avatar_step();
        sched_sleep_ticks(2);
    }
}

void space_missile_thread(void)
{
    while (1){
        space_missile_step();
        sched_sleep_ticks(3);
    }
}

void space_invaders_thread(void)
{
    while (1){
        space_invaders_step();
        space_explosion_step();

        if (space.level_change_delay) {
            --space.level_change_delay;
            if (!space.level_change_delay){
                ++space.level;
                space_init_level();
            }
        }

        sched_sleep_ticks(15);
    }
}

void space_init(void)
{
    space_explosion_shape[0] = ' ';
    space_explosion_shape[1] = SPACE_EXPLOTION_SMALL;
    space_explosion_shape[2] = SPACE_EXPLOTION_BIG;
    space_explosion_shape[3] = SPACE_EXPLOTION_SMALL;
    space_explosion_shape[4] = SPACE_EXPLOTION_BIG;
    space_explosion_shape[5] = SPACE_EXPLOTION_SMALL;
    space_explosion_shape[6] = SPACE_EXPLOTION_BIG;
    space_explosion_shape[7] = SPACE_EXPLOTION_SMALL;

    lcdinit();
    space_create_lcd_custom_chars();
    space.done  = 0;
    space.level = 1;
    space_start_page();
    space_init_level();
    joystick_init();

}

void space_run_multi_threaded()
{
    // in order of priority
    sched_fork(space_avatar_thread);
    sched_fork(space_missile_thread);
    sched_fork(space_invaders_thread);
}

void space_run_single_threaded()
{
    while(!space.done) {
            usleep(1000);
            space_step();
    }
}

